跳到主要内容

MySQL 中的乐观锁和悲观锁

基础概念类面试题

1. 什么是悲观锁和乐观锁?它们的核心思想有什么区别?

答案要点:

  • 悲观锁(Pessimistic Locking):假设在整个事务过程中会发生并发冲突,因此默认对数据加锁,防止其他事务修改数据
  • 乐观锁(Optimistic Locking):假设事务之间很少发生冲突,不直接加锁,而是在提交时检查是否发生冲突

2. 在MySQL中如何实现悲观锁?请给出具体的SQL示例

答案要点:

  • 使用 SELECT ... FOR UPDATE 语句
  • 使用 LOCK TABLES 语句
-- 方式1:行级悲观锁
START TRANSACTION;
SELECT stock FROM products WHERE id = 'product_id' FOR UPDATE;
-- 检查库存并执行业务逻辑
UPDATE products SET stock = stock - 1 WHERE id = 'product_id';
COMMIT;

-- 方式2:表级悲观锁
LOCK TABLES products WRITE;
-- 执行业务逻辑
UNLOCK TABLES;

3. 在MySQL中如何实现乐观锁?请详细说明实现方式

答案要点:

  • 使用版本号(Versioning)
  • 使用时间戳
  • 使用CAS(Compare and Set)操作
-- 版本号方式
-- 1. 查询当前版本号
SELECT version FROM posts WHERE id = 'post_id';

-- 2. 更新时检查版本号
UPDATE posts
SET title = 'New Title', content = 'New Content', version = version + 1
WHERE id = 'post_id' AND version = 'current_version';

-- 3. 检查影响行数判断是否更新成功

场景应用类面试题

4. 请设计一个电商系统的库存扣减方案,要求支持高并发

考察点:

  • 对业务场景的理解
  • 锁机制的选择
  • 性能优化思路

参考答案结合时序图:

说明: 悲观锁方案确保了数据一致性,但在高并发场景下会产生锁等待,影响性能。适用于写操作频繁的场景。

5. 如果上述场景改用乐观锁实现,请画出时序图并说明优缺点

说明: 乐观锁方案允许并发读取,只在更新时检查冲突,提高了并发性能,但需要处理重试逻辑。适用于读多写少的场景。

深度技术类面试题

6. 为什么不直接使用事务,而要使用乐观锁?它们有什么本质区别?

答案要点:

  • 事务:提供ACID特性,是数据库层面的并发控制机制
  • 乐观锁:应用层面的并发控制策略
  • 区别
    1. 事务涉及锁竞争,高并发时可能产生性能瓶颈
    2. 乐观锁避免长时间锁定,提高并发性能
    3. 乐观锁需要开发人员处理冲突逻辑
    4. 适用场景不同:事务适合复杂业务,乐观锁适合读多写少

7. 比较三种并发控制方式的特点和适用场景

详细对比:

方式加锁时机冲突处理并发性能适用场景
事务操作过程中自动回滚中等复杂业务流程
悲观锁读取时加锁阻塞等待较低写操作频繁
乐观锁更新时检查手动重试较高读多写少

8. 在Go语言中,如何实现一个支持乐观锁的数据访问层?

考察点:

  • Go语言数据库操作
  • 错误处理
  • 重试机制设计
// 产品结构体
type Product struct {
ID int64 `db:"id"`
Name string `db:"name"`
Stock int `db:"stock"`
Version int64 `db:"version"`
}

// 乐观锁更新接口
type OptimisticLocker interface {
UpdateWithOptimisticLock(ctx context.Context, id int64, updateFunc func(*Product) error) error
}

// 实现乐观锁更新
func (r *ProductRepository) UpdateWithOptimisticLock(ctx context.Context, id int64, updateFunc func(*Product) error) error {
maxRetries := 3

for i := 0; i < maxRetries; i++ {
// 1. 查询当前数据和版本号
product, err := r.GetByID(ctx, id)
if err != nil {
return err
}

// 2. 执行业务逻辑
if err := updateFunc(product); err != nil {
return err
}

// 3. 更新数据,检查版本号
result, err := r.db.ExecContext(ctx,
`UPDATE products SET stock = ?, version = version + 1
WHERE id = ? AND version = ?`,
product.Stock, id, product.Version)

if err != nil {
return err
}

// 4. 检查影响行数
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}

if rowsAffected > 0 {
return nil // 更新成功
}

// 版本冲突,重试
time.Sleep(time.Millisecond * time.Duration(rand.Intn(10)))
}

return errors.New("optimistic lock failed after retries")
}

9. 在分布式系统中,单机的乐观锁还够用吗?如何解决?

考察点:

  • 分布式系统理解
  • 分布式锁机制
  • Redis等中间件应用

答案要点:

  • 单机乐观锁在分布式环境下不够用
  • 需要使用分布式锁:Redis、ZooKeeper、etcd
  • 可以使用数据库的分布式锁机制
  • 考虑使用消息队列进行异步处理

场景:分布式系统中的库存扣减

解决方案:完整的分布式乐观锁方案

10. 设计一个博客系统的并发编辑功能,要求多人可以同时编辑但要避免数据覆盖

性能优化类面试题

11. 在高并发场景下,如何优化乐观锁的性能?

答案要点:

  • 减少重试次数和间隔
  • 使用随机退避算法
  • 业务层面的优化(预扣库存)
  • 分段锁机制
  • 异步处理非关键操作

12. 什么情况下应该选择悲观锁而不是乐观锁?

答案要点:

  • 写操作非常频繁的场景
  • 冲突率较高的业务
  • 重试成本很高的操作
  • 对数据一致性要求极高的场景
  • 业务逻辑复杂,回滚代价大的情况